package dhadi.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;

import dhadi.util.Logger;

public class CpuPlayer extends Player {
    
    private static long THINK_DELAY = 1000*1;
    private Stack<Move> mPastMoves = new Stack<Move>(); 
    
    public CpuPlayer(String name) {
        super(name);
        mPastMoves = new Stack<Move>();
    }

    public void makeAMove() {
        Thread t = new Thread(new Runnable() {
            public void run() {
                // It looks like cpu is thinking
                try {Thread.sleep(THINK_DELAY);}catch (InterruptedException e) {}
                CpuPlayer.this.thinkAMove();
            }
        }, "MakeAMove-Thread");
        t.start();
    }

    private void thinkAMove() {
        List<Pick> unUsedPicks = this.board.currentPlayer().getUnUsedPicks();
        Move bestMove = null;
        if (!unUsedPicks.isEmpty()) {
            bestMove = placePickOnBoard();
        } else {
            bestMove = movePickOnBoard();
        }
        
        if (bestMove != null) {
            mPastMoves.push(bestMove);
            this.board.move(bestMove);
        } else {
            Logger.log(CpuPlayer.class, "Could not think a move !");
        }
    }    

    private Move placePickOnBoard() {
        List<Pick> unUsedPicks = this.board.currentPlayer().getUnUsedPicks();
        Pick p = unUsedPicks.size() != 0?unUsedPicks.get(0):null;
        Move m = null;
        // look for possible dhadi
        Node possibleDhadi = findPossibleDhadi();
        if (possibleDhadi != null)
        {
            m = new Move(p, this.board.currentPlayer());
            m.setToNode(possibleDhadi);
            //board.move(m);
            return m;
        }

        // Block opponent dhadi formation
        Node dhadiBlocker = preventDhadi();
        if (dhadiBlocker != null) {
            m = new Move(p, this.board.currentPlayer());
            m.setToNode(dhadiBlocker);
            //board.move(m);
            return m;
        }
        
        Node cornerBlocker = GameStrategy.findCornerStrategyNode(this,this.board);
        if (cornerBlocker != null) {
            m = new Move(p, this.board.currentPlayer());
            m.setToNode(cornerBlocker);
            return m;
        }
        
        // Block opponent L - Strategy
        dhadiBlocker = GameStrategy.findLstrategyNode(this,this.board);
        if (dhadiBlocker != null) {
            m = new Move(p, this.board.currentPlayer());
            m.setToNode(dhadiBlocker);
            //board.move(m);
            return m;
        } else {
            List<Pick> onboardPicks = this.getPicksOnBoard();
            List<Node> nodes = this.board.getEmptyNodes();
            // Place first pick randomly.
            if (onboardPicks.isEmpty()) {
                int randIndex = (int)((Math.random()*100)%nodes.size());
                m = new Move(p, this.board.currentPlayer());
                m.setToNode(nodes.get(randIndex));
                //board.move(m);
                return m;
            } else {
                // Form L Strategy
                Node strategyNode = formDoubleDhadi();
                if (strategyNode != null) {
                    m = new Move(p, this.board.currentPlayer());
                    m.setToNode(strategyNode);
                    //board.move(m);
                    return m;
                } else {
                    int randIndex = (int)((Math.random()*100)%nodes.size());
                    m = new Move(p, this.board.currentPlayer());
                    m.setToNode(nodes.get(randIndex));
                    //board.move(m);
                    return m;
                }                   
            }
        }
    }

    private Move movePickOnBoard() {
        
        /**
         * Algorithm
         * 
         * Find picks that can move, of them
         * Rate each move according to the following
         * order
         * 
         * 1. (R-MAX) Search for dhadi moves, and make dhadi
         *  
         * 2. (R-MIN) Do not move pick that could make dhadi 
         *     to opponent in next move
         *
         * 3. (R1) Do not move pick that could open dhadi 
         *     to opponent in next move
         *  
         * 4. (R5) Move a pick that can block opponent dhadi
         *   
         * 5. PENDING
         *    (R3) Move the pick towards forming a dhadi
         *    1. Make sure opponent cannot block dhadi
         *   
         * 6. OBSOLETE
         *    (R4) Move a dhadi pick so that
         *    1. Opponent cannot block such dhadi
         *    2. Opponent has no immediate dhadi to kill
         *    
         *   
         * 7. (R2) Find the pick that can 
         *    1. block many opponent picks and
         *    2. opens minimum moves
         *    
         * Use the move that has heavyWeight
         */
        double rank = 0.0;
        double r_min = rank++;
        double r_1   = rank++;
        double r_2   = rank++;
        double r_3   = rank++;
        double r_4   = rank++;
        double r_5   = rank++;
        double r_max = rank++;
        
        List<Pick> movablePicks = this.board.findMovablePicks(this);
        if (movablePicks.isEmpty()) {
            Logger.log(CpuPlayer.class,"WARNING: No movable picks! this should not happen");
            return null;
        }
        
        WeightedList<Move> weightedMoves = new WeightedList<Move>();
        
        // 1. (R-MAX) Search for dhadi moves, and make dhadi
        List<Move> dhadiMoves = GameStrategy.findDhadiMove(this,movablePicks,this.board);
        weightedMoves.add(dhadiMoves,r_max);
        
        // 2. (R-MIN) Do not move pick that could make dhadi 
        //     to opponent in next move
        List<Move> movesThatMakeDhadiForOpponent = GameStrategy.findMovesThatMakeDhadiForOpponent(this,movablePicks,this.board);
        weightedMoves.add(movesThatMakeDhadiForOpponent,r_min);
        
        // 3. (R1) Do not move pick that could open dhadi 
        //     to opponent in next move
        List<Move> movesThatOpenDhadiForOpponent = GameStrategy.findMovesThatOpenDhadiForOpponent(this,movablePicks,this.board);
        weightedMoves.add(movesThatOpenDhadiForOpponent,r_1);

        // 4. (R5) Move a pick that can block opponent dhadi
        List<Move> oppDhadiBlockers = GameStrategy.findOpponentDhadiBlockers(this,movablePicks,this.board);
        weightedMoves.add(oppDhadiBlockers,r_5);
        
        //
        // 7. (R2) Find the pick that can 
        //    1. block many opponent picks and
        //    2. opens minimum moves
        WeightedList<Move> bestBlockingMoves = 
            GameStrategy.findBestBlockingMove(this,movablePicks,this.board);
        weightedMoves.add(bestBlockingMoves, r_2);
        
        
        Move bestMove = weightedMoves.getHeavyWeight();
        
        // IDENTIFY CYCLES BY READING THE PREVIOUS MOVES
        // AND BREAK THEM BY TAKING ALTERNATE MOVE.
        if (this.isCyclicMove(bestMove)) {
            StringBuilder msg = new StringBuilder();
            msg.append("Prevented cyclic move, causingMove: ").append(bestMove);
            List<Move> excludeList = new ArrayList<Move>();
            excludeList.add(bestMove);
            Move nextBestMove = weightedMoves.getHeavyWeight(excludeList);
            if (nextBestMove != null) {
                msg.append(", nextBestMove: ").append(nextBestMove);
                bestMove = nextBestMove;
                Logger.log(CpuPlayer.class, msg.toString());
            }
        }
        
        //this.board.move(bestMove);
        return bestMove;
    }           

    /**
     * Returns true if the given move is part of
     * pastMoves and is just before the last move.
     * The idea is to break cyclic moves.
     * 
     * @param move
     * @return
     */
    private boolean isCyclicMove(Move move) {
        if (mPastMoves.size() <= 2) return false;
        Move prevMove = mPastMoves.get(mPastMoves.size()-2);
        return prevMove.equals(move);
    }

    /**
     * 
     * @return
     */
    private Node formDoubleDhadi() {
        /**
         * Alogirthm
         * 
         * Step#1 Use previously formed L Strategy
         * Step#2 If not found, Form L Strategy
         * 
         * Step#1 Same as Strategy.findLstrategyNode()
         * 
         * Step#2 Algorithm
         * 
         *  1. For each onboard pick
         *    1.1. find adjacent Triodes, filter out if a triode is occupied by opponent
         *    1.2. For each filtered adjacent tiodes, find tangent triodes 
         *         and add into 'tangentTriodes'
         *    1.3. From tangentTriodes filter out opponent occupied triodes 
         *         (opponent picks or own picks)
         *    1.4. From tangentTriodes filter out adjacent triodes (step#1)
         *    1.5. For each adjacent triodes
         *         5.1 For each tangentTriodes
         *         5.2 continue if tangentTriode is not
         *             intersecting with touching triode
         *         5.3 Find non intersecting Nodes on 
         *             tangentTriode and add it to 'PossibleNodes'
         *             
         *   3. If possibleNodes are empty then abort.
         *  
         *   4. From 'possibleNodes' find max count of each Node.
         *  
         *   5. Return the Node which has max count, other wise 
         *      return an orbitrary node.
         */

        /**
         * In Opponents view find the L Strategy Node,
         * that would be our double dhadi maker.
         */
        Node node = GameStrategy.findLstrategyNode(this.getOpponent(), board);
        if (node != null) return node;

        WeightedList<Node> possibleNodes = new WeightedList<Node>(); 

        //1. For each onboard pick
        List<Pick> onBoardPicks = this.getPicksOnBoard();
        for (Pick onBoardPick : onBoardPicks) {
            //1.1. find adjacent Triodes...
            List<String> adjacentTriodes = board.findTouchingTriodes(onBoardPick.getNode());
            List<String> filteredAdjacentTriodes = new ArrayList<String>();
            //1.1  ...filter out if touching node is occupied by opponent
            for (String adjacentTriode : adjacentTriodes) {
                if (this.board.getPlayerPicksOnNodes(
                        this.board.getTriodeNodes(adjacentTriode), this.getOpponent()).size() == 0)
                    filteredAdjacentTriodes.add(adjacentTriode);
            }
            adjacentTriodes = filteredAdjacentTriodes;

            //1.2. For each filtered adjacent tiodes, find tangent triodes 
            //     and add into 'tangentTriodes'
            List<String> tangentTriodes = new ArrayList<String>();
            for (String adjacentTriode : filteredAdjacentTriodes) {
                tangentTriodes.addAll(board.findTouchingTriodes(adjacentTriode));
            }

            //1.3. From tangentTriodes filter out opponent occupied triodes 
            //     (opponent picks or own picks)
            List<String> filteredTangentTriodes = new ArrayList<String>();
            for (String tangentTriode : tangentTriodes) {
                if (this.board.isTriodeEmpty(tangentTriode))
                    filteredTangentTriodes.add(tangentTriode);
            }
            tangentTriodes = filteredTangentTriodes;

            //1.4. From tangentTriodes filter out adjacent triodes (step#1)
            for (String adjacentTriode : adjacentTriodes) {
                tangentTriodes.remove(adjacentTriode);
            }

            //1.5. For each adjacent triodes
            for (String adjacentTriode : adjacentTriodes) {
                //5.1 For each tangentTriodes
                for (String tangentTriode : tangentTriodes) {
                    //5.2 continue if tangentTriode is not
                    //    intersecting with touching triode
                    Node intersectingNode = this.board.findTriodeIntersection(adjacentTriode, tangentTriode);
                    if (intersectingNode == null) continue;

                    List<Node> tangentTriodeNodes = this.board.getTriodeNodes(tangentTriode);
                    for (Node n : tangentTriodeNodes) {
                        if (n.equals(intersectingNode)) continue;
                        possibleNodes.add(n);
                    }
                }
            }
        }

        // 3. If possibleNodes are empty then abort.
        if (possibleNodes.isEmpty()) return null;

        // 4. From 'possibleNodes' find max count of each Node.
        // 5. Return the Node which has max count, other wise 
        //    return an orbitrary node.
        return (Node)possibleNodes.getHeavyWeight();		
    }

    private Node findPossibleDhadi() {
        List<Node> emptyNodes = this.board.getEmptyNodes();
        Set<Pick> nonDhadiPicks = this.getOpponent().getPossiblePicksToKill(); // confusing, need to change the method name
        List<Pick> onBoardPicks = this.getPicksOnBoard();
        Map<String, List<Pick>> rankedTriodes = 
            findRankedTriodes(nonDhadiPicks,
                    onBoardPicks);

        // Pick the triode whose spread is 2, this
        // is where we place the pick to make a dhadi.
        Set<Entry<String, List<Pick>>> entrySet = rankedTriodes.entrySet();
        for (Entry<String, List<Pick>> rankedTriode : entrySet) {
            if (rankedTriode.getValue().size() > 1) {
                String triode = rankedTriode.getKey();
                List<Node> emptyNodes1 = this.board.getEmptyNodes(triode);
                if (emptyNodes1.size() > 0) return emptyNodes1.get(0);
            }
        }
        return null;
    }

    /**
     * 
     * @return
     */
    private Node preventDhadi() {
        /* Algorithm
		   1. Find empty nodes
           2. Find opponent non dhadi formation picks
           3. For each opponent pick, find triodes.
              3.1 for each triode, check only opponent picks positioned
                  3.1.1 based on the opponent spread, rank triodes
           4. Use one of the lowest ranked triode, and fill the pick
         */
        List<Node> emptyNodes = this.board.getEmptyNodes();
        Set<Pick> opponentNonDhadiPicks = 
            this.getPossiblePicksToKill();
        List<Pick> opponentOnBoardPicks = this.getOpponent().getPicksOnBoard();
        Map<String, List<Pick>> rankedTriodes = 
            findRankedTriodes(opponentNonDhadiPicks,
                    opponentOnBoardPicks);

        // Pick the triode whose spread is 2, this 
        // is where we place a pick to block dhadi
        Set<Entry<String, List<Pick>>> entrySet = rankedTriodes.entrySet();
        for (Entry<String, List<Pick>> rankedTriode : entrySet) {
            if (rankedTriode.getValue().size() > 1) {
                String triode = rankedTriode.getKey();
                List<Node> emptyNodes1 = this.board.getEmptyNodes(triode);
                if (emptyNodes1.size() > 0) return emptyNodes1.get(0);
            }
        }

        return null;
    }

    /**
     * Ranks triods on which the given nonDhadiPicks can
     * form a possible dhadi.
     * 
     * Ranking (1,2) is based on the following
     * Rank 1 - two nodes are empty on a triode
     * Rank 2 - only one node is empty on a triode
     * 
     * Return value is a Map between triode and List of picks
     * on the triode.
     * 
     * The rank of a triode is its List size in the Map
     *  
     * @param nonDhadiPicks
     * @param onBoardPciks
     * @return
     */
    private Map<String, List<Pick>> findRankedTriodes(
            Set<Pick> nonDhadiPicks,
            List<Pick> onBoardPciks) {
        Map<String, List<Pick>> rankedTriodes = new HashMap<String, List<Pick>>();
        for (Pick opponentPick : nonDhadiPicks) {
            List<String> triodes = this.board.findTriods(opponentPick.getNode());
            for (String triode : triodes) {
                List<Pick> triodePicks = this.findPicksOnTriodes(triode,onBoardPciks);
                rankedTriodes.put(triode, triodePicks);
            }
        }
        return rankedTriodes;
    }

    public void onlyView() {

    }

    public void dhadiOnYou() {
        super.dhadiOnYou();
    }

    public void dhadiByYou() {
        super.dhadiByYou();
        
        Thread t = new Thread(new Runnable(){
            public void run() {
                // It looks like cpu is thinking
                try {Thread.sleep(THINK_DELAY);}catch (InterruptedException e) {}
                CpuPlayer.this.killAPick();
            }
        },"DhadiByYou-Thread");
        t.start();
    }

    private void killAPick() {
        Move m = null;
        Pick p = null;
        Set<Pick> picksToKill = this.getPossiblePicksToKill();
        
        // Can't kill any pick this time.
        if (picksToKill.isEmpty()) return;
        
        /**
         * Killing a pick            
         *   1. kill a pick that opens dhadi
         *   2. Select an arbitrary pick to kill - non dhadi picks
         */
        p = this.findDhadiMakingPickToKill(picksToKill);
        if (p != null) {
            m = Move.createKill(p, this.board.currentPlayer());
            board.move(m);
            return;
        } 
        
        p = picksToKill.iterator().hasNext()?picksToKill.iterator().next():null;
        if (p != null)
        {
            m = Move.createKill(p, this.board.currentPlayer());
            board.move(m);
            return; 
        }
        
        return;
    }

    private Pick findDhadiMakingPickToKill(Set<Pick> picksToKill) {
        /**
         *   Algorithm
         *   
         *   1. get opponents possible picks to kill.
         *   2. For each possible pick
         *      2.1 Find adjacent triodes
         *      2.2 For each adjacent triodes
         *          2.2.1 Find number of own nodes occupied
         *          2.2.2 If occupied nodes equal 2 then return pick.
         *   3. return nothing.
         */
        
        for (Pick pick : picksToKill) {
            List<String> adjacentTriodes =
                this.board.findTouchingTriodes(pick.getNode());
            for (String adjacentTriode : adjacentTriodes) {
                List<Pick> triodePicks = 
                    this.board.getPlayerPicksOnTriode(adjacentTriode, this);
                if (triodePicks.size() == 2) return pick;
            }
        }
        return null;
    }
}